package collaboration.server;

import java.io.IOException;

import collaboration.client.CollabClient;

import ocsf.server.ConnectionToClient;
import ocsf.server.ObservableServer;

/**
 * The <code>CollabServer</code> extends <code>ObservableServer</code> and contains 
 * all the methods necessary to host a <code>CollabServer</code>.
 * 
 * @author Trevor Clack 6286191
 * @author Bowen Cheng 6204467
 * @version November 26, 2012 (1.44)
 */
public class CollabServer extends ObservableServer {
	
	//------------------------
	// CLASS VARIABLES
	//------------------------
	
	/**
	 * The newline character to use.
	 */
	final public static String NEWLINE = System.getProperty("line.separator");

	/**
	 * The name of the program.
	 */
	final public static String PROGRAM_NAME = "Collaboration Server";

	/**
	 * The version number of the program.
	 */
	final public static String PROGRAM_VERSION = "1.44";

	/**
	 * The key used to retrieve the loginID for each client on the server.
	 */	
	final public static String CLIENT_ID = "loginID";

	/**
	 * The key used to retrieve isHost for each client on the server.
	 */	
	final public static String IS_HOST = "isHost";

	/**
	 * The server message prefix for outgoing messages from the server.
	 */	
	final protected static String SERVER_MSG_PREFIX = "SERVER MSG>";

	/**
	 * The Commands and their descriptions.
	 */
	final public static String[][] COMMANDS = { // If adding new commands append them at the end of the array!!
		{"*quit","Stops listening for new clients, disconnects" + 
			" all clients from the server, and terminates " + PROGRAM_NAME + "."},
		{"*stop","Stops listening for new clients but keeps " + PROGRAM_NAME + " running."},
		{"*close","Stops listening for new clients then disconnects" + 
			" all clients from the server but keeps " + PROGRAM_NAME + " running."},
		{"*setport","Changes the port to listen on." + 
			" The server must be closed to run \"*setport\"!"},
		{"*start","Starts listening for new clients." + 
			" The server must be stopped to run \"*start\"!"},
		{"*getport","Displays the current port number."},
		{"*debug", "Turn debug on or off. Toggles between on and off."}
	};

	//------------------------
	// MEMBER VARIABLES
	//------------------------

	/**
	 * If the server is in Debug mode.
	 */	
	private boolean debug;
	
	/**
	 * The default host.
	 */
	final public static String DEFAULT_HOST = "localhost";

	/**
	 * The default port.
	 */
	final public static int DEFAULT_PORT = 5700;


	//collabServer Associations
	/**
	 * This instance of <code>CollabClient</code>.
	 */
	private CollabClient collabClient;



	//------------------------
	// CONSTRUCTOR
	//------------------------
	
	/**
	 * The constructor for <code>CollabServer</code>.
	 * 
	 * @param port the port to listen for connections on.
	 * @param aCollabClient The instance of <code>CollabClient</code>.
	 * @throws A <code>RuntimeException</code> if the <code>CollabClient</code> instance was not set.
	 */
	public CollabServer(int port, CollabClient aCollabClient) throws RuntimeException
	{
		super(port);
		boolean didAddCollabClient = setCollabClient(aCollabClient);
		if (!didAddCollabClient)
		{
			throw new RuntimeException("Unable to create collabServer due to collabClient");
		}
	}



	/**
	 * This private method sends server messages to the appropriate interface.
	 * 
	 * @param msg The message to send to the interface.
	 */
	private void sendToServerUI(String msg){
		StringBuffer message = new StringBuffer("^server," + msg);
		if(getCollabClient()!=null && getCollabClient().getCollabClientUI() != null)
			getCollabClient().getCollabClientUI().display(message);
		message = null;
	}

	/**
	 * This overridden method handles any messages received from the client.
	 *
	 * @param msg The message received from the client.
	 * @param client The connection from which the message originated.
	 */
	@Override
	public void handleMessageFromClient(Object msg, ConnectionToClient client)
	{
		if (msg != null) { /* Lots of null messages received if client
							 terminates badly. */
			if (msg.getClass().equals(String.class)){// Check if message is a String
				if(debug){
					sendToServerUI("Message received: " + msg + " from " + client.getInfo(CLIENT_ID));
				}
				String message = msg.toString();
				if(message.startsWith("#logoff")) {
					message = client.getInfo(CLIENT_ID) + " has logged off.";
					try {
						client.close();
					} catch (IOException e) {
						if(debug)
						{sendToServerUI("Error logging off " + client.getInfo(CLIENT_ID));}
					}
					sendToServerUI(message);
					this.sendToAllClients(SERVER_MSG_PREFIX + message);
				}else if(message.startsWith("#login")) {
					String[] loginArgs = message.split(" ");
					if(client.getInfo(CLIENT_ID) != null){
						try {
							client.sendToClient(SERVER_MSG_PREFIX + "Error: You are already logged in!");
						} catch (IOException e) {
							//Do nothing
						}
					}else if((loginArgs.length > 1) && (!loginArgs[1].equals(""))){
						//Check for duplicate LoginID
						Thread[] connectedClients = getClientConnections();
						String[] clientIDs = new String[connectedClients.length];
						for(int id = 0; id < clientIDs.length; id++){
							clientIDs[id] = (String) ((ConnectionToClient) connectedClients[id]).getInfo(CLIENT_ID);
						}
						int count = 0;
						for(int id = 0; id < clientIDs.length; id++){
							if(loginArgs[1].equals(clientIDs[id])){
								count++;
							}
							//Clean up as we go
							clientIDs[id]=null;
						}
						//Clean up
						clientIDs=null;
						if(count > 0){ // LoginId is already in use
							try {
								client.sendToClient(SERVER_MSG_PREFIX + "Error: That loginID is already in use!");
							} catch (IOException e) {
								//Do Nothing
							}
							try {
								client.close();
							} catch (IOException e1) {
								//Do nothing
							}
							return;//Don't go any further, stop here
						}
						//end of check for duplicates
						if(collabClient.getLoginID().equals(loginArgs[1])){//This is the host
							client.setInfo(IS_HOST, 1);
						}else{
							client.setInfo(IS_HOST, 0);
						}
						client.setInfo(CLIENT_ID, loginArgs[1]);
						// get the loginID from the server instead of from the raw input.
						message = client.getInfo(CLIENT_ID) + " has logged on.";
						sendToServerUI(message);
						sendToAllClients(SERVER_MSG_PREFIX + message);
					}else{//if for some reason there is a client who sends a blank login 
						try {
							client.sendToClient(SERVER_MSG_PREFIX + "Error: You must provide a loginID!");
						} catch (IOException e) {
							//Do Nothing
						}
						try {
							client.close();
						} catch (IOException e1) {
							//Do nothing
						}
					}
				}else if(client.getInfo(CLIENT_ID) != null){
					this.sendToAllClients(client.getInfo(CLIENT_ID) + "> " + message);
				}else {// client has not logged on to the server.
					try {
						client.sendToClient(SERVER_MSG_PREFIX + "Error: You must be logged in to send messages!");
					} catch (IOException e) {
						//Do Nothing
					}
					try {
						client.close();
					} catch (IOException e1) {
						//Do nothing
					}
				}
			}else if(msg.getClass().equals(StringBuffer.class)){// Check if message is a StringBuffer
				sendToAllClients(msg);// forward on the Application commands
			}// Do nothing if msg is not a String or StringBuffer
		}// Do nothing if msg is null.
	}

	/**
	 * This method gets the program name.
	 * 
	 * @return Returns the program name
	 */
	public String getProgramName(){
		return PROGRAM_NAME;
	}

	/**
	 * This method gets the program version.
	 * 
	 * @return Returns the program version
	 */
	public String getProgramVersion(){
		return PROGRAM_VERSION;
	}

	/**
	 * This method handles all data coming from the UI            
	 *
	 * @param message The message from the UI.    
	 */
	public void handleMessageFromServerUI(String message)
	{
		if (message.startsWith("*")) { // All server commands should start with *
			String[] clientString =  message.split(" ");// Get the command and any other parameters
			int clientCommand = -1; // Initialize to -1 for command not found
			if(message.equals("*")){clientCommand = -2;}// Client is asking for the list of commands
			for(int i = 0; i < COMMANDS.length; i++){
				if(clientString[0].equalsIgnoreCase(COMMANDS[i][0])){
					clientCommand = i;
				}
			}
			if(!runCommand(clientCommand,clientString) && debug){
				String errorMessage = "Command \"" + clientString[0] + 
						"\" did not run correctly! " + NEWLINE +
						"Please check your syntax!" + NEWLINE +
						"-------------------------------------------" + NEWLINE +
						COMMANDS[clientCommand][0] + ": " + COMMANDS[clientCommand][1] + 
						NEWLINE + "-------------------------------------------" ;
				sendToServerUI(errorMessage);
			}
		} else {
			this.sendToAllClients(SERVER_MSG_PREFIX + message);
		}
	}

	/**
	 * This method prints the list of valid commands.
	 */
	public void displayCommands(){
		String message = "Valid commands are:" + NEWLINE;
		message += "-------------------------------------------" + NEWLINE;
		for (int i = 0; i < COMMANDS.length; i++){
			message += COMMANDS[i][0] + ": " + COMMANDS[i][1] + NEWLINE;
			message += "-------------------------------------------" + NEWLINE;
		}
		sendToServerUI(message);
	}

	/**
	 * This private method runs the command that was input by the user.
	 * 
	 * @param int The index of the command to run.
	 * @param String[] An array containing the command and its' parameters.
	 * @return A boolean indicating if the command successfully ran.
	 */
	private boolean runCommand(int commandIndex, String[] commandString){
		boolean ranCommand = false;
		switch (commandIndex){
		case -2:// display commands
			displayCommands();
			ranCommand = true;
			break;
		case 0: // *quit
			handleMessageFromServerUI("*close");// stops listening, disconnects clients, and closes the server.
			sendToServerUI("Terminating \"" + PROGRAM_NAME + "\"!");
			ranCommand = true;
			this.getCollabClient().setCollabServer(null);
			this.delete();// Terminates the server
			break;
		case 1: //*stop
			sendToServerUI("Attempting to stop listening.");
			if(getCollabClient().getCollabServer() != null)
			{
				if (this.isListening()) {
					this.stopListening();
					sendToServerUI("Server has stopped listening for new connections.");
					this.sendToAllClients(SERVER_MSG_PREFIX + "Server has stopped listening for new connections.");
					ranCommand = true;
				}else{
					sendToServerUI("The server has already stopped listening.");
				}
			}
			else
			{
				sendToServerUI("Error: There is no server running!");
			}
			break;
		case 2: //*close
			if(getCollabClient().getCollabServer() != null)
			{
				handleMessageFromServerUI("*stop");// Stops the server.
				sendToServerUI("Attempting to logoff clients and close the server.");
				this.sendToAllClients(SERVER_MSG_PREFIX + "SERVER IS SHUTTING DOWN! DISCONNECTING!");
				try {
					Thread clients[] = getClientConnections();
					//Close the connection for all the clients
					for(int client = 0; client < clients.length; client++){
						try{
							((ConnectionToClient) clients[client]).close();
						}catch (Exception e){
							//Do Nothing
						}
					}
					this.close();
					sendToServerUI("Clients have been logged off and the server has been closed.");
					ranCommand = true;
				} catch (IOException e) {
					sendToServerUI("Error: \"" + e + "\" while attempting to close server!");
				}
			}
			else
			{
				sendToServerUI("Error: There is no server running!");
			}
			break;
		case 3: //*setport
			if(getCollabClient().getCollabServer() != null)
			{
				if(!this.isListening() && this.getNumberOfClients()==0){
					try{
						int result = Integer.parseInt(commandString[1]);
						if(result >= 1024 && result <= 65535){
						this.setPort(result);
						ranCommand = true;
						}else{
							sendToServerUI("Invalid port number " + result + "!" + NEWLINE + 
									"The port should be between 1024 and 65535.");
						}
					}// If an exception is thrown change nothing.
					catch(Throwable t)
					{
						if(debug)
							sendToServerUI("Error: " + t + "!" + NEWLINE + "Port not changed!");
					}
				}
				//Print the current port number.
				handleMessageFromServerUI("*getport");
			}
			else
			{
				sendToServerUI("Error: There is no server running!");
			}
			break;
		case 4: //*start
			if(getCollabClient().getCollabServer() != null)
			{
				if(!this.isListening()){
					sendToServerUI("Attempting to start listening server on port " + this.getPort() + ".");
					try {
						this.listen();
						if(!getCollabClient().isConnected()){
							getCollabClient().setHost(DEFAULT_HOST);
							getCollabClient().setPort(getPort());
							getCollabClient().handleMessageFromClientUI("#login");
						}
					} catch (Throwable t) {
						sendToServerUI("Error: \"" + t + "\" while attempting to listen for clients!");
					}
					ranCommand = true;
				}
			}
			else
			{
				sendToServerUI("Error: There is no server running!");
			}
			break;
		case 5: //*getport
			sendToServerUI("The port is set to \"" + this.getPort() + "\".");
			ranCommand = true;
			break;
		case 6: //*debug
			String message;
			if(!debug)
			{
				debug = true;
				message = "debug is on";
			}
			else
			{
				debug = false;
				message = "debug is off";
			}
			sendToServerUI(message);
			ranCommand = true;
			break;
		default:
			sendToServerUI(commandString[0] + " is not a valid command!");
			if(debug)
				displayCommands();
			ranCommand = true;
			break;
		}
		return ranCommand;
	}

	/**
	 * This method overrides the one in the superclass.  Called
	 * when the server starts listening for connections.
	 */
	@Override
	protected void serverStarted()
	{
		sendToServerUI("Server listening for connections on port " + getPort());
	}

	/**
	 * This method overrides the one in the superclass.  Called
	 * when the server stops listening for connections.
	 */
	@Override
	protected void serverStopped()
	{
		sendToServerUI("Server has stopped listening for connections.");
	}

	/**
	 * This method overrides the one in the superclass.  Called
	 * when a client connects.
	 */
	@Override
	protected void clientConnected(ConnectionToClient client) {
		if(debug)
			sendToServerUI("A new client at " + client + " has connected.");
	}

	/**
	 * This method overrides the one in the superclass.  Called
	 * when a client disconnects.
	 */
	@Override
	synchronized protected void clientDisconnected(ConnectionToClient client) {
		if(debug){
			sendToServerUI("Client " + client.getInfo(CLIENT_ID) + " has disconnected.");
			this.sendToAllClients(SERVER_MSG_PREFIX + client.getInfo(CLIENT_ID) + " has disconnected.");
		}
	}

	/**
	 * This method overrides the one in the superclass.  Called
	 * when a client disconnects with an exception.
	 */
	@Override
	synchronized protected void clientException(ConnectionToClient client,
			Throwable exception) {
		String message = (String) client.getInfo(CLIENT_ID);
		if(debug) {
			message +=" has thrown exception \"" + exception + "\" and";
		}
		message+=" has disconnected.";
		sendToServerUI(message);
		this.sendToAllClients(SERVER_MSG_PREFIX + client.getInfo(CLIENT_ID) + " has disconnected.");
	}

	/* Code from Umple - edited*/
	
	//------------------------
	// INTERFACE
	//------------------------

	/**
	 * This method gets the instance of <code>CollabClient</code>.
	 * 
	 * @return The instance of <code>CollabClient</code>.
	 */
	public CollabClient getCollabClient()
	{
		return collabClient;
	}

	/**
	 * This method sets this instance of <code>CollabClient</code>.
	 * Returns true if the instance was set and false otherwise.
	 * 
	 * @param aCollabClient This instance of <code>CollabClient</code>.
	 * @return A boolean indicating true if the instance was set and false otherwise.
	 */
	public boolean setCollabClient(CollabClient newCollabClient)
	{
		boolean wasSet = false;
		if (newCollabClient == null)
		{
			//Unable to setCollabClient to null, as collabServer must always be associated to a collabClient
			return wasSet;
		}

		CollabServer existingCollabServer = newCollabClient.getCollabServer();
		if (existingCollabServer != null && !equals(existingCollabServer))
		{
			//Unable to setCollabClient, the current collabClient already has a collabServer, which would be orphaned if it were re-assigned
			return wasSet;
		}

		CollabClient oldCollabClient = collabClient;
		collabClient = newCollabClient;
		collabClient.setCollabServer(this);

		if (oldCollabClient != null)
		{
			oldCollabClient.setCollabServer(null);
		}
		wasSet = true;
		return wasSet;
	}

	/**
	 * This method deletes all the elements used by this instance of <code>CollabServer</code>.
	 */
	public void delete()
	{
		CollabClient existingCollabClient = collabClient;
		collabClient = null;
		if (existingCollabClient != null)
		{
			existingCollabClient.setCollabServer(null);
		}
	}
}
